Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OZ L02; Spearbit 87; Enforce strict abi encoding #346

Merged
merged 26 commits into from
Sep 6, 2024
Merged

Conversation

hensha256
Copy link
Contributor

No description provided.

@hensha256 hensha256 changed the base branch from better-calldata-checks to main September 6, 2024 00:19
0age
0age previously approved these changes Sep 6, 2024
@hensha256 hensha256 changed the title Enforce strict abi encoding OZ L02; Spearbit 87; Enforce strict abi encoding Sep 6, 2024
@hensha256 hensha256 merged commit 151b282 into main Sep 6, 2024
3 checks passed
@hensha256 hensha256 deleted the 0age-calldata branch September 6, 2024 00:42
let relativeOffset := sub(params.offset, _bytes.offset)
// Check that that isn't longer than the bytes themselves, or revert
if lt(_bytes.length, add(params.length, relativeOffset)) {
for { let offset := 0 } lt(offset, tailOffset) { offset := add(offset, 32) } {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exceedingly minor consistency nit: 32 used here whereas elsewhere the hex 0x20 is used

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we love a nit

expectedOffset := add(expectedOffset, length)
}

// if the data encoding was invalid, or the provided bytes string isnt as long as the encoding says, revert
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isnt --> isn't

/// @notice equivalent to SliceOutOfBounds.selector, stored in least-significant bits
uint256 constant SLICE_ERROR_SELECTOR = 0x3b99b53d;

/// @dev equivalent to: abi.decode(params, (bytes, bytes[])) in calldata (requires strict abi encoding)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be better to note here that the masking makes it not quite strictly equivalent.

// Verify actions offset matches strict encoding
let invalidData := xor(calldataload(_bytes.offset), 0x40)
actions.offset := add(_bytes.offset, 0x60)
actions.length := and(calldataload(add(_bytes.offset, 0x40)), OFFSET_OR_LENGTH_MASK)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here the action.length value is silently trimmed to 32 bytes, which could hide errors in abi encoding. Consider doing instead:

actions.length := calldataload(add(_bytes.offset, 0x40))
invalidData := or(invalidData, shr(32, actions.length))

let paramsPtr := add(_bytes.offset, calldataload(add(_bytes.offset, 0x20)))
// Strict encoding requires that the data begin with:
// 0x00: 0x40 (offset to `actions.length`)
// 0x20: 0x60 + actions.length (offset to `params.length`)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is inaccurate, as the actions.length value is actually aligned to the word boundary.

actions.length := calldataload(actionsPtr)
params.length := calldataload(paramsPtr)
// Round actions length up to be word-aligned, and add 0x60 (for the first 3 words of encoding)
let paramsLengthOffset := add(and(add(actions.length, 0x1f), OFFSET_OR_LENGTH_MASK_AND_WORD_ALIGN), 0x60)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here an intermediate value is silently trimmed to 32 bits, which could hide overflow errors, and could potentially be exploited. Consider doing instead:

let paramsLengthOffset := shl(5, shr(5, add(actions.length, 0x1f)))
invalidData := or(invalidData, shr(32, paramsLengthOffset))

actions.length := calldataload(actionsPtr)
params.length := calldataload(paramsPtr)
// Round actions length up to be word-aligned, and add 0x60 (for the first 3 words of encoding)
let paramsLengthOffset := add(and(add(actions.length, 0x1f), OFFSET_OR_LENGTH_MASK_AND_WORD_ALIGN), 0x60)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the 0x60 value is added after applying the mask, the result could actually exceed 32 bites.

// Verify params offset matches strict encoding
invalidData := or(invalidData, xor(calldataload(add(_bytes.offset, 0x20)), paramsLengthOffset))
let paramsLengthPointer := add(_bytes.offset, paramsLengthOffset)
params.length := and(calldataload(paramsLengthPointer), OFFSET_OR_LENGTH_MASK)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here the params.length value is silently trimmed to 32 bytes, which could hide errors in abi encoding. Consider doing instead:

params.length := calldataload(paramsLengthPointer)
invalidData := or(invalidData, shr(32, actions.length))

// The offset of the `_arg`-th element is `32 * arg`, which stores the offset of the length pointer.
// shl(5, x) is equivalent to mul(32, x)
let lengthPtr :=
add(_bytes.offset, and(calldataload(add(_bytes.offset, shl(5, _arg))), OFFSET_OR_LENGTH_MASK))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overflow is possible when shifting left and when adding. Consider reverting on overflow.

// The offset of the `_arg`-th element is `32 * arg`, which stores the offset of the length pointer.
// shl(5, x) is equivalent to mul(32, x)
let lengthPtr :=
add(_bytes.offset, and(calldataload(add(_bytes.offset, shl(5, _arg))), OFFSET_OR_LENGTH_MASK))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here the lengthPtr value is silently trimmed to 32 bytes, which could hide errors in abi encoding. Consider reverting in case the calculated value exceeds 32 bits.

let lengthPtr :=
add(_bytes.offset, and(calldataload(add(_bytes.offset, shl(5, _arg))), OFFSET_OR_LENGTH_MASK))
// the number of bytes in the bytes string
length := and(calldataload(lengthPtr), OFFSET_OR_LENGTH_MASK)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here the length value is silently trimmed to 32 bytes, which could hide errors in abi encoding. Consider reverting in case the length value exceeds 32 bits.

/// @notice Decode the `_arg`-th element in `_bytes` as `bytes`
/// @param _bytes The input bytes string to extract a bytes string from
/// @param _arg The index of the argument to extract
function toBytes(bytes calldata _bytes, uint256 _arg) internal pure returns (bytes calldata res) {
(uint256 length, uint256 offset) = toLengthOffset(_bytes, _arg);
uint256 length;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable is redundant. Just use res.length instead.

// the number of bytes in the bytes string
length := and(calldataload(lengthPtr), OFFSET_OR_LENGTH_MASK)
// the offset where the bytes string begins
let offset := add(lengthPtr, 0x20)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable is redundant. Just use res.offset instead.

// 0x60: beginning of actions

// Verify actions offset matches strict encoding
let invalidData := xor(calldataload(_bytes.offset), 0x40)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_bytes.offset is always 0, can save some gas here and below. decodeActionsRouterParams is always called on calldata that contains only an encoded ABI.

// 0x60: beginning of actions

// Verify actions offset matches strict encoding
let invalidData := xor(calldataload(_bytes.offset), 0x40)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test case: failure when the first byte is not 0x40.

// Round actions length up to be word-aligned, and add 0x60 (for the first 3 words of encoding)
let paramsLengthOffset := add(and(add(actions.length, 0x1f), OFFSET_OR_LENGTH_MASK_AND_WORD_ALIGN), 0x60)
// Verify params offset matches strict encoding
invalidData := or(invalidData, xor(calldataload(add(_bytes.offset, 0x20)), paramsLengthOffset))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A failure here is not tested.

for { let offset := 0 } lt(offset, tailOffset) { offset := add(offset, 32) } {
let itemLengthOffset := calldataload(add(params.offset, offset))
// Verify that the offset matches the expected offset from strict encoding
invalidData := or(invalidData, xor(itemLengthOffset, expectedOffset))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not tested when fails.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants